package com.dbflow5.processor.definition;

import com.dbflow5.annotation.ConflictAction;
import com.dbflow5.annotation.Database;
import com.dbflow5.processor.ClassNames;
import com.dbflow5.processor.ProcessorManager;
import com.dbflow5.processor.Validators;
import com.dbflow5.processor.utils.JavaPoetExtensions;
import com.dbflow5.processor.utils.ProcessorUtils;
import com.squareup.javapoet.*;

import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Description: Writes [Database] definitions,
 * which contain [Table], [ModelView], and [Migration]
 */
public class DatabaseDefinition extends BaseDefinition implements TypeDefinition {

    private final int databaseVersion;
    private final boolean foreignKeysSupported;
    private final boolean consistencyChecksEnabled;
    private final boolean backupEnabled;

    public ConflictAction insertConflict;
    public ConflictAction updateConflict;

    public DatabaseObjectHolder objectHolder = null;

    public DatabaseDefinition(Database database, ProcessorManager manager, Element element) {
        super(element, manager, ClassNames.FLOW_MANAGER_PACKAGE);

        databaseVersion = database.version();
        foreignKeysSupported = database.foreignKeyConstraintsEnforced();
        consistencyChecksEnabled = database.consistencyCheckEnabled();
        backupEnabled = database.backupEnabled();

        insertConflict = database.insertConflict();
        updateConflict = database.updateConflict();

        setOutputClassName(elementName + "_Database");

        if (!element.getModifiers().contains(Modifier.ABSTRACT)
                || element.getModifiers().contains(Modifier.PRIVATE)
                || !ProcessorUtils.isSubclass(typeElement, manager.processingEnvironment, ClassNames.BASE_DATABASE_DEFINITION_CLASSNAME)) {
            manager.logError(elementClassName + " must be a visible abstract class that " +
                    "extends " + ClassNames.BASE_DATABASE_DEFINITION_CLASSNAME);
        }
    }

    @Override
    public TypeName extendsClass() {
        return elementClassName;
    }

    @Override
    public void onWriteDefinition(TypeSpec.Builder typeBuilder) {
        writeConstructor(typeBuilder);
        writeGetters(typeBuilder);
    }

    public void validateAndPrepareToWrite() {
        prepareDefinitions();
        validateDefinitions();
    }

    private void validateDefinitions() {
        if(elementClassName != null) {
            Map<TypeName, TableDefinition> map = new HashMap<>();
            Validators.TableValidator tableValidator = new Validators.TableValidator();
            manager.getTableDefinitions(elementClassName).stream()
                    .filter(tableDefinition -> tableValidator.validate(ProcessorManager.manager, tableDefinition))
                    .forEach(tableDefinition -> {
                        if(tableDefinition.elementClassName != null) {
                            map.put(tableDefinition.elementClassName, tableDefinition);
                        }
                    });
            manager.setTableDefinitions(map, elementClassName);

            Map<TypeName, ModelViewDefinition> modelViewDefinitionMap = new HashMap<>();
            Validators.ModelViewValidator modelViewValidator = new Validators.ModelViewValidator();
            manager.getModelViewDefinitions(elementClassName)
                    .stream().filter(modelViewDefinition -> modelViewValidator.validate(ProcessorManager.manager, modelViewDefinition)).forEach(modelViewDefinition -> {
                if(modelViewDefinition.elementClassName != null) {
                    modelViewDefinitionMap.put(modelViewDefinition.elementClassName, modelViewDefinition);
                }
            });

            manager.setModelViewDefinitions(modelViewDefinitionMap, elementClassName);
        }
    }

    private void prepareDefinitions() {
        if(elementClassName != null) {
            manager.getTableDefinitions(elementClassName).forEach(TableDefinition::prepareForWrite);
            manager.getModelViewDefinitions(elementClassName).forEach(ModelViewDefinition::prepareForWrite);
            manager.getQueryModelDefinitions(elementClassName).forEach(QueryModelDefinition::prepareForWrite);
        }
    }

    private void writeConstructor(TypeSpec.Builder builder) {
        MethodSpec.Builder methodBuilder = MethodSpec.constructorBuilder();
        ParameterSpec.Builder paramsBuilder = ParameterSpec.builder(ClassNames.DATABASE_HOLDER, "holder");
        methodBuilder.addParameter(paramsBuilder.build());

        methodBuilder.addModifiers(Modifier.PUBLIC);

        if(elementClassName != null) {
            for (TableDefinition definition : manager.getTableDefinitions(elementClassName)) {
                if (definition.hasGlobalTypeConverters()) {
                    methodBuilder.addStatement("addModelAdapter(new $T(holder, this), holder)", definition.outputClassName);
                } else {
                    methodBuilder.addStatement("addModelAdapter(new $T(this), holder)", definition.outputClassName);
                }
            }

            for (ModelViewDefinition definition : manager.getModelViewDefinitions(elementClassName)) {
                if (definition.hasGlobalTypeConverters()) {
                    methodBuilder.addStatement("addModelViewAdapter(new $T(holder, this), holder)", definition.outputClassName);
                } else {
                    methodBuilder.addStatement("addModelViewAdapter(new $T(this), holder)", definition.outputClassName);
                }
            }

            for (QueryModelDefinition definition : manager.getQueryModelDefinitions(elementClassName)) {
                if (definition.hasGlobalTypeConverters()) {
                    methodBuilder.addStatement("addRetrievalAdapter(new $T(holder, this), holder)", definition.outputClassName);
                } else {
                    methodBuilder.addStatement("addRetrievalAdapter(new $T(this), holder)", definition.outputClassName);
                }
            }

            Map<Integer, List<MigrationDefinition>> migrationDefinitionMap = manager.getMigrationsForDatabase(elementClassName);
            migrationDefinitionMap.keySet()
                    .stream().sorted((a, b) -> a > b ? -1 : 1)
                    .forEach(version -> {
                        migrationDefinitionMap.get(version).stream().sorted(Comparator.comparing(it -> it.priority))
                        .forEach(migrationDefinition -> methodBuilder.addStatement("addMigration("+version+", new $T"+migrationDefinition.constructorName+")", migrationDefinition.elementClassName));
                    });

        }
        builder.addMethod(methodBuilder.build());
    }

    private void writeGetters(TypeSpec.Builder typeBuilder) {
        JavaPoetExtensions.overrideFun(typeBuilder, ParameterizedTypeName.get(ClassName.get(Class.class), WildcardTypeName.subtypeOf(Object.class)), "getAssociatedDatabaseClassFile", builder -> {
            builder.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
            builder.addStatement("return $T.class", elementTypeName);
            return null;
        });

        JavaPoetExtensions.overrideFun(typeBuilder, TypeName.BOOLEAN, "isForeignKeysSupported", builder -> {
            builder.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
            builder.addStatement("return " + foreignKeysSupported);
            return null;
        });

        JavaPoetExtensions.overrideFun(typeBuilder, TypeName.BOOLEAN, "backupEnabled", builder -> {
            builder.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
            builder.addStatement("return " + backupEnabled);
            return null;
        });

        JavaPoetExtensions.overrideFun(typeBuilder, TypeName.BOOLEAN, "areConsistencyChecksEnabled", builder -> {
            builder.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
            builder.addStatement("return " + consistencyChecksEnabled);
            return null;
        });

        JavaPoetExtensions.overrideFun(typeBuilder, TypeName.INT, "getDatabaseVersion", builder -> {
            builder.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
            builder.addStatement("return " + databaseVersion);
            return null;
        });
    }
}
